/**
 * 做的修改
 * 增加断线重连机制
 */

'use strict';

const WS_CONNECTING = 0;       // ws 已经连接
const WS_OPEN       = 1;       // ws 已经打开
const WS_CLOSING    = 2;       // ws 正在关闭
const WS_CLOSED     = 3;       // ws 已经关闭

const Events = {
  "ERROR"             : "error",              // 错误
  "IOERROR"           : "ioError",            // io错误
  "HEARTBEAT_TIMEOUT" : "heartbeatTimeout",   // 心跳超时
  "CLOSE"             : "close",              // 断开连接，onKick,服务端关闭，disconnect都会触发
  "PARSE_ERROR"       : "parseError",         // 解析服务端返回时发生异常
  "ONKICK"            : "onKick"              // 服务器主动踢出
};


let EventEmitter = require("./EventEmitter");
let Protobuf     = require("./protobuf");
let Protocol     = require("./protocol");
let WebSocket    = require('ws');
let util         = require('util');

const JS_WS_CLIENT_TYPE    = 'js-websocket';
const JS_WS_CLIENT_VERSION = '0.0.1';

const RES_OK         = 200;
const RES_FAIL       = 500;
const RES_OLD_CLIENT = 501;

let client = function () {
  this.Package  = Protocol.Package;
  this.Message  = Protocol.Message;

  this.Events = Events;

  this.socket       = null;
  this.reqId        = 0;
  this.callbacks    = {};
  this.handlers     = {};
  this.handlers[this.Package.TYPE_HANDSHAKE] = this.handshake.bind(this);
  this.handlers[this.Package.TYPE_HEARTBEAT] = this.heartbeat.bind(this);
  this.handlers[this.Package.TYPE_DATA] = this.onData.bind(this);
  this.handlers[this.Package.TYPE_KICK] = this.onKick.bind(this);

  this.pomeloProtos = null;                      // 握手数据，可以使用外部传入

  //Map from request id to route
  this.routeMap     = {};
  this.dict         = {};    // route string to code
  this.abbrs        = {};   // code to route string
  this.serverProtos = {};
  this.clientProtos = {};
  this.protoVersion = 0;

  // 心跳处理
  this.heartbeatInterval    = 0;
  this.heartbeatTimeout     = 0;
  this.nextHeartbeatTimeout = 0;
  this.gapThreshold         = 100;   // heartbeat gap threashold
  this.heartbeatId          = null;
  this.heartbeatTimeoutId   = null;
  this.heartbeatStartTime   = 0;
  this.heartbeatPing        = 0;
  this.handshakeCallback    = null;

  this.decode = null;
  this.encode = null;

  this.handshakeBuffer = {
    'sys': {
      type   : JS_WS_CLIENT_TYPE,
      version: JS_WS_CLIENT_VERSION
    },
    'user': {
    }
  };

  this.initCallback = null;

  this.ws_url    = "";

  this.connected  = false;      // 是否连接成功
  this.closeCount = 0;          // 连接断开次数
};

util.inherits(client, EventEmitter);

let pro = client.prototype;

/**
 * 初始化连接的函数
 * @param params{Object}  eg.{host:"localhost",port:"3010"}
 * @param cb{Function}    初始化完成后回调
 */
pro.init = function(params, cb){
  let host = params.host;
  let port = params.port;

  this.encode = params.encode || defaultEncode.bind(this);
  this.decode = params.decode || defaultDecode.bind(this);

  console.log('encode: ' + !!params.encode);

  let url = 'ws://' + host;
  if(port) {
    url +=  ':' + port;
  }

  this.ws_url = url;

  this.handshakeBuffer.user = params.user;
  this.handshakeCallback = params.handshakeCallback;
  this.initWebSocket(url, cb);
};

/**
 * 断开连接的函数
 */
pro.disconnect = function() {
  try{
    if(this.socket) {
      if(this.socket.disconnect) this.socket.disconnect();
      if(this.socket.close) this.socket.close();

      console.log("【网络】断开游戏服务器连接");

      this.clearSocket(this.socket);
      this.socket           = null;
    }

    if(this.heartbeatId) {
      clearTimeout(this.heartbeatId);
      this.heartbeatId = null;
    }
    if(this.heartbeatTimeoutId) {
      clearTimeout(this.heartbeatTimeoutId);
      this.heartbeatTimeoutId = null;
    }
  }
  catch(e){
    console.log("【网络】" + e);
  }
};

/**
 * 发送请求，会有结果返回
 * @param route{String} 协议路由
 * @param msg{Object}   消息,如果定义了protobuf，则require字段必须有数据
 * @param cb{Function}  消息处理函数,参数是json数据 cb(json)
 */
pro.request = function(route, msg, cb) {
  if(arguments.length === 2 && typeof msg === 'function') {
    cb = msg;
    msg = {};
  } else {
    msg = msg || {};
  }
  route = route || msg.route;
  if(!route) {
    return;
  }

  this.reqId++;
  this.sendMessage(this.reqId, route, msg);

  this.callbacks[this.reqId] = cb;
  this.routeMap[this.reqId] = route;
};

/**
 * 给服务端发送通知
 * @param route{String} 协议路由
 * @param msg{Object}   消息,如果定义了protobuf，则require字段必须有数据
 */
pro.notify = function(route, msg) {
  msg = msg || {};
  this.sendMessage(0, route, msg);
};

pro.getPing = function(){
  return this.heartbeatPing;
};

/** 设置握手数据（暂未使用） */
pro.setHandshake = function(protos){
  this.pomeloProtos = protos;
};

pro.clearSocket = function(socket){
  let nope = function() {};

  socket.onopen    = nope;
  socket.onmessage = nope;
  socket.onerror   = nope;
  socket.onclose   = nope;
};

let defaultDecode = function(data) {
  //probuff decode
  let msg = this.Message.decode(data);

  if(msg.id > 0){
    msg.route = this.routeMap[msg.id];
    delete this.routeMap[msg.id];
    if(!msg.route){
      console.log("【网络】协议路由不存在");
      return;
    }
  }

  msg.body = this.deCompose(msg);
  return msg;
};

let defaultEncode = function(reqId, route, msg) {
  let type = reqId ? this.Message.TYPE_REQUEST : this.Message.TYPE_NOTIFY;

  //compress message by protobuf
  if (this.clientProtos && this.clientProtos[route]) {
    msg = Protobuf.encode(route, msg);
  } else {
    msg = Protocol.strencode(JSON.stringify(msg));
  }

  let compressRoute = 0;
  if (this.dict && this.dict[route]) {
    route = this.dict[route];
    compressRoute = 1;
  }

  return this.Message.encode(reqId, type, compressRoute, route, msg);
};

pro.initWebSocket = function(url,cb) {
  this.initCallback = cb;

  //Add protobuf version
  if (this.protoVersion === 0){
    let protos;

    try{
      // 外部设置的数据比本地保存的数据优先级高
      if(this.pomeloProtos){
        protos = this.pomeloProtos;
      }
    }
    catch(err){
      protos = null;
    }

    if(protos){
      this.protoVersion = protos.version || 0;
      this.serverProtos = protos.server || {};
      this.clientProtos = protos.client || {};

      if(Protobuf) Protobuf.init({encoderProtos: this.clientProtos, decoderProtos: this.serverProtos});
    }
  }

  //Set protoversion
  this.handshakeBuffer.sys.protoVersion = this.protoVersion;

  let createSocket = function(){
    this.disconnect();

    this.socket            = new WebSocket(url);
    this.socket.binaryType = 'arraybuffer';
    this.socket.onopen     = onopen;
    this.socket.onmessage  = onmessage;
    this.socket.onerror    = onerror;
    this.socket.onclose    = onclose;
  }.bind(this);

  /** 连接成功 */
  let onopen = function(event){
    this.closeCount = 0;
    this.connected  = true;
    console.log("【网络】连接成功");
    let obj = this.Package.encode(this.Package.TYPE_HANDSHAKE, Protocol.strencode(JSON.stringify(this.handshakeBuffer)));
    this.send(obj);
  }.bind(this);

  /** 接收数据 */
  let onmessage = function(event) {
    this.processPackage(this.Package.decode(event.data), cb);
    // new package arrived, update the heartbeat timeout
    if(this.heartbeatTimeout) {
      this.nextHeartbeatTimeout = Date.now() + this.heartbeatTimeout;
    }
  }.bind(this);

  /** 网络错误 */
  let onerror = function(event) {

    console.log("【网络】连接游戏服务器出错");
  };

  /** 网络断开 */
  let onclose = function(event){
    if (this.connected === false && this.closeCount < 3){
      this.closeCount++;
      console.log("【网络】WEBSOCKET 重握手");
      createSocket();
    }
    else {
      console.log("【网络】WEBSOCKET 断开");
      this.closeCount = 0;
      this.connected  = false;
      this.socket     = null;
      this.emit(Events.CLOSE);
    }
  }.bind(this);

  if(this.socket){
    this.clearSocket(this.socket);
  }

  console.log('【网络】游戏服务器连接地址：' + url);

  createSocket();
};

pro.sendMessage = function(reqId, route, msg) {
  if(this.encode) {
    msg = this.encode(reqId, route, msg);
  }

  let packet = this.Package.encode(this.Package.TYPE_DATA, msg);
  this.send(packet);
};

pro.send = function(packet){
  if(this.socket === null || this.socket.readyState !== WS_OPEN){
    console.log("【网络】发送数据给服务器时检查与服务器已断开");
    // pomelo.emit(Events.CLOSE);
  }
  else {
    this.socket.send(packet);
  }
};

pro.heartbeat = function(data) {
  if(this.heartbeatStartTime){
    this.heartbeatPing = (new Date()) - this.heartbeatStartTime;
  }

  if(!this.heartbeatInterval) {
    // no heartbeat
    return;
  }

  var obj = this.Package.encode(this.Package.TYPE_HEARTBEAT);
  if(this.heartbeatTimeoutId) {
    clearTimeout(this.heartbeatTimeoutId);
    this.heartbeatTimeoutId = null;
  }

  if(this.heartbeatId) {
    // already in a heartbeat interval
    return;
  }

  this.heartbeatId = setTimeout(function() {
    if (this.socket === null || this.socket.readyState !== WS_OPEN) return;

    this.heartbeatId = null;
    this.heartbeatStartTime = new Date();
    this.send(obj);

    this.nextHeartbeatTimeout = Date.now() + this.heartbeatTimeout;
    this.heartbeatTimeoutId   = setTimeout(this.heartbeatTimeoutCb, this.heartbeatTimeout);

  }.bind(this), this.heartbeatInterval);
};

pro.heartbeatTimeoutCb = function() {
  let gap = this.nextHeartbeatTimeout - Date.now();
  if(gap > this.gapThreshold) {
    this.heartbeatTimeoutId = setTimeout(this.heartbeatTimeoutCb, gap);
  } else {
    this.emit(Events.HEARTBEAT_TIMEOUT);
    this.disconnect();
  }
};

pro.handshake = function(data){
  data = JSON.parse(Protocol.strdecode(data));
  if(data.code === RES_OLD_CLIENT) {
    this.emit(Events.ERROR, 'client version not fullfill');
    return;
  }

  if(data.code !== RES_OK) {
    this.emit(Events.ERROR, 'handshake fail');
    return;
  }

  this.handshakeInit(data);

  let obj = this.Package.encode(this.Package.TYPE_HANDSHAKE_ACK);
  this.send(obj);
  if(this.initCallback) {
    this.initCallback(this.socket);
    this.initCallback = null;
  }
};

pro.onData = function(data) {
  let msg = data;
  if(this.decode) {
    msg = this.decode(msg);
  }
  this.processMessage(msg);
};

pro.onKick = function (data) {
  data = Protocol.strdecode(data);

  try {
    data = JSON.parse(data);
  } catch(e) {
    console.log(e.message);
  }
  this.emit(Events.ONKICK, data);
};

pro.processPackage = function(msg) {
  this.handlers[msg.type](msg.body);
};

pro.processMessage = function(msg) {
  if(!msg.id) {
    // server push message
    this.emit(msg.route, msg.body);
    return;
  }

  //if have a id then find the callback function with the request
  let cb = this.callbacks[msg.id];

  delete this.callbacks[msg.id];
  if(typeof cb !== 'function') {
    return;
  }

  try{
    cb(msg.body);
  }
  catch(e){
    this.emit(Events.PARSE_ERROR,e);
  }
};

pro.deCompose = function(msg){
  let route = msg.route;

  // Decompose route from dict
  if(msg.compressRoute) {
    if(!this.abbrs[route]){
      return {};
    }

    route = msg.route = this.abbrs[route];
  }
  if (this.serverProtos && this.serverProtos[route]){
    return Protobuf.decode(route, msg.body);
  }
  else{
    // cc.warn("【网络】协议需要使用 protobuf 压缩：" + route);
    return JSON.parse(Protocol.strdecode(msg.body));
  }
};

pro.handshakeInit = function(data){
  if(data.sys && data.sys.heartbeat) {
    this.heartbeatInterval = data.sys.heartbeat * 1000;   // heartbeat interval
    this.heartbeatTimeout = this.heartbeatInterval * 2;        // max heartbeat timeout
  } else {
    this.heartbeatInterval = 0;
    this.heartbeatTimeout = 0;
  }

  this.initData(data);

  if(typeof this.handshakeCallback === 'function') {
    this.handshakeCallback(data.user);
  }
};

pro.initData = function(data){
  if(!data || !data.sys) {
    return;
  }
  this.dict = data.sys.dict;
  let protos = data.sys.protos;

  //Init compress dict
  if(this.dict){
    this.abbrs = {};

    for(var route in this.dict){
      this.abbrs[this.dict[route]] = route;
    }
  }

  //Init Protobuf protos
  if(protos){
    this.protoVersion = protos.version || 0;
    this.serverProtos = protos.server || {};
    this.clientProtos = protos.client || {};
    if(!!Protobuf){
      Protobuf.init({encoderProtos: protos.client, decoderProtos: protos.server});
    }

    //Save protobuf protos to localStorage
    // localStorage.setItem('protos', JSON.stringify(protos));
  } else {
    // localStorage.setItem('protos', JSON.stringify({}));
  }
};

module.exports = client;


